﻿using System;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using System.Reflection;
using System.Diagnostics.Contracts;

namespace FoundationLib.Common
{
	/// <summary>
	/// Provides options for data sorting.
	/// </summary>
	public class SortingOptions
	{
		/// <summary>
		/// The name of the property to order by
		/// </summary>
		public string PropertyName { get; set; }
		/// <summary>
		/// The sorting direction
		/// </summary>
		public SortingDirection Direction { get; set; }

		
		/// <summary>
		/// Swaps the sorting direction.
		/// </summary>
		/// <returns>The new <see cref="SortingOptions"/> instance</returns>
		public SortingOptions SwapDirection()
		{
			if (this.Direction == SortingDirection.Ascending)
				this.Direction = SortingDirection.Descending;
			else
				this.Direction = SortingDirection.Ascending;
			return this;
		}

		/// <summary>
		/// Parses the given string into a new <see cref="SortingOptions"/> instance.
		/// </summary>
		/// <param name="sortString">The sort string</param>
		/// <returns>The <see cref="SortingOptions"/> instance representing the given sort string</returns>
		public static SortingOptions Parse(string sortString)
		{
			if (String.IsNullOrWhiteSpace(sortString))
				return null;

			Regex pattern = new Regex("( )|(,)");
			string[] parts = pattern.Split(sortString);
			if (parts.Length == 0)
				return null;
			else if (parts.Length == 1)
				return new SortingOptions { PropertyName = sortString.Trim(), Direction = SortingDirection.Ascending };
			
			SortingOptions options = new SortingOptions { PropertyName = parts[0].Trim() };
			if (parts[2].Trim().ToLower() == "desc" || parts[2].Trim().ToLower() == "descending")
				options.Direction = SortingDirection.Descending;
			else
				options.Direction = SortingDirection.Ascending;

			return options;
		}

		/// <summary>
		/// Parses the given property name and direction into a <see cref="SortingOptions"/> instance.
		/// </summary>
		/// <param name="propertyName">The property name to order by</param>
		/// <param name="direction">The sorting direction (either 'asc' or 'desc')</param>
		/// <returns>The sorting options</returns>
		public static SortingOptions Parse(string propertyName, string direction)
		{
			return Parse(String.Join(" ", propertyName, direction));
		}

		/// <summary>
		/// Parses the given member expression into a new <see cref="SortingOptions"/> instance.
		/// </summary>
		/// <typeparam name="T">The object type</typeparam>
		/// <param name="expression">The member expression pointing to a property to order by</param>
		/// <param name="direction">The sorting direction</param>
		/// <returns>The sorting options</returns>
		public static SortingOptions Parse<T>(Expression<Func<T, object>> expression, SortingDirection direction)
		{
			Contract.Requires(expression != null);
			string propertyPath = ExpressionHelper.PropertyPathFor(expression);
			SortingOptions options = new SortingOptions { PropertyName = propertyPath, Direction = direction };
			return options;
		}

		/// <summary>
		/// Returns the lambda expression representing these sorting options.
		/// </summary>
		/// <typeparam name="T">The object type</typeparam>
		/// <returns>A lambda expression representing these sorting options</returns>
		public Expression<Func<T, object>> LambdaExpression<T>()
		{
			Type dataType = typeof(T);
			ParameterExpression rootParameter = Expression.Parameter(dataType, "x");
			Expression propertyPath = rootParameter;
			if (this.PropertyName.Contains('.'))
			{
				string[] parts = this.PropertyName.Split('.');
				foreach (string part in parts)
				{
					PropertyInfo pInfo = dataType.GetProperty(part);
					propertyPath = Expression.Property(propertyPath, pInfo);
					dataType = pInfo.PropertyType;
				}
			}
			else
			{
				propertyPath = Expression.Property(rootParameter, this.PropertyName);
			}

			Expression<Func<T, object>> lambda = Expression.Lambda<Func<T, object>>(Expression.Convert(propertyPath, typeof(object)), rootParameter);
			return lambda;
		}

		/// <summary>
		/// Returns a method call expression which can be executed on a set of <typeparamref name="T"/> instances.
		/// </summary>
		/// <typeparam name="T">The object type</typeparam>
		/// <param name="source">The data source</param>
		/// <returns>The method call expression</returns>
		public MethodCallExpression MethodCallExpression<T>(IQueryable<T> source)
		{
			Type dataType = typeof(T);
			LambdaExpression lambda = this.LambdaExpression<T>();
			string orderMethod = this.Direction == SortingDirection.Descending ? "OrderByDescending" : "OrderBy";

			MethodCallExpression methodExpression = Expression.Call(
				typeof(Queryable),
				orderMethod,
				new Type[] { dataType, typeof(object) },
				source.Expression,
				Expression.Quote(lambda));

			return methodExpression;
		}
	}
}
